package org.zhang.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import groovy.util.logging.Slf4j;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.zhang.shortlink.common.convention.exception.ClientException;
import org.zhang.shortlink.project.common.constants.RedisConstant;
import org.zhang.shortlink.project.dao.entity.*;
import org.zhang.shortlink.project.dao.mapper.*;
import org.zhang.shortlink.project.dto.req.ShortLinkCreateDTO;
import org.zhang.shortlink.project.dto.req.ShortLinkPageDTO;
import org.zhang.shortlink.project.dto.req.ShortLinkUpdateDTO;
import org.zhang.shortlink.project.dto.resp.ShortLinkCreateVO;
import org.zhang.shortlink.project.dto.resp.ShortLinkGroupCountVO;
import org.zhang.shortlink.project.dto.resp.ShortLinkPageVO;
import org.zhang.shortlink.project.service.ShortLinkService;
import org.zhang.shortlink.project.utils.HashUtil;
import org.zhang.shortlink.project.utils.LinkUtil;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.zhang.shortlink.project.common.constants.RedisConstant.GOTO_SHORT_IS_NULL_LINK_KEY;
import static org.zhang.shortlink.project.utils.LinkUtil.getActualIp;
import static org.zhang.shortlink.project.utils.LinkUtil.getOs;

/**
 * @author zhang
 * @description 针对表【t_link】的数据库操作Service实现
 * @createDate 2024-04-08 20:33:12
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO>
        implements ShortLinkService {

    private final RBloomFilter<String> shortLinkCreateCachePenetrationBloomFilter;

    private final ShortLinkGotoMapper shortLinkGotoMapper;

    private final ShortLinkAccessStatsMapper shortLinkAccessStatsMapper;

    private final StringRedisTemplate redisTemplate;

    private final RedissonClient redissonClient;

    private final LinkLocaleStatsMapper linkLocaleStatsMapper;

    private final LinkOsStatsMapper linkOsStatsMapper;

    private final LinkBrowserStatsMapper linkBrowserStatsMapper;

    private final LinkAccessLogsMapper linkAccessLogsMapper;

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Value("${short-link.domain.default}")
    private String domain;

    @Value("${short-link.stats.local.amap-key}")
    private String amapKey;

    /**
     * 新增短链接
     *
     * @param requestParam
     * @return ShortLinkCreateVO
     */
    @Override
    public ShortLinkCreateVO saveShortLink(ShortLinkCreateDTO requestParam) {
        String shortUri = generateSuffix(requestParam);

        //完整短链接
        String fullShortUrl = domain + "/" + shortUri;

        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .shortUri(shortUri)
                .domain(domain)
                .fullShortUrl(fullShortUrl)
                .enableStatus(1)
                .build();
        BeanUtil.copyProperties(requestParam, shortLinkDO, "domain");

        baseMapper.insert(shortLinkDO);

        //新增路由配置
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .gid(shortLinkDO.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        shortLinkGotoMapper.insert(linkGotoDO);
        //缓存预热
        redisTemplate.opsForValue().set(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()
                , LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS);
        //添加到布隆过滤器
        shortLinkCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateVO.builder()
                .gid(shortLinkDO.getGid())
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .originUrl(shortLinkDO.getOriginUrl())
                .build();
    }

    /**
     * 分页查询短链接
     *
     * @param requestParam
     * @return IPage<ShortLinkPageVO>
     */
    @Override
    public IPage<ShortLinkPageVO> pageShortLink(ShortLinkPageDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> wrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getEnableStatus, "1");
        IPage<ShortLinkDO> page = baseMapper.selectPage(requestParam, wrapper);
        return page.convert(each -> BeanUtil.toBean(each, ShortLinkPageVO.class));
    }

    /**
     * 根据gid列表获取分组中的短链接数
     *
     * @param requestParam
     * @return List<ShortLinkGroupCountVO>
     */
    @Override
    public List<ShortLinkGroupCountVO> listCountGroup(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = new QueryWrapper<ShortLinkDO>()
                .select("gid ,count(*) as shortLinkCount")
                .in("gid", requestParam)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        List<ShortLinkGroupCountVO> collect = maps.stream().map(map -> BeanUtil.toBean(map, ShortLinkGroupCountVO.class)).toList();
        return collect;
    }

    /**
     * TODO 修改短链接
     *
     * @param requestParam ShortLinkUpdateDTO
     * @return Void
     */
    @Override
    public Void updateShortLink(ShortLinkUpdateDTO requestParam) {
//        //1、查询短链接是否修改分组
//        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
//                .eq(ShortLinkDO::getGid, requestParam.getGid())
//                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
//                .eq(ShortLinkDO::getEnableStatus, 1);
//        ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
//
//        if(shortLinkDO==null){
//
//        }else {
//
//        }
        return null;
    }

    /**
     * 短链接跳转
     *
     * @param shortLink
     * @param request
     * @param response
     * @return
     */
    @Override
    public void restoreUrl(String shortLink, ServletRequest request, ServletResponse response) throws IOException {
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortLink;

        //缓存穿透：1、判断布隆过滤器
        if (!shortLinkCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        //获取缓存
        String originUrl = redisTemplate.opsForValue().get(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 获取到缓存跳转
        if (StrUtil.isNotBlank(originUrl)) {
            //统计数据
            shortLinkStats(fullShortUrl, request, response);
            // 302重定向
            ((HttpServletResponse) response).sendRedirect(originUrl);
            return;
        }
        //未获取到缓存
        //缓存穿透：2、获取空对象
        String obj = redisTemplate.opsForValue().get(String.format(GOTO_SHORT_IS_NULL_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(obj)) {
            //缓存穿透：3、获取到了空对象返回
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        //获取互斥锁
        RLock lock = redissonClient.getLock(String.format(RedisConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            //缓存穿透：4、双重判定
            obj = redisTemplate.opsForValue().get(String.format(GOTO_SHORT_IS_NULL_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(obj)) {
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            originUrl = redisTemplate.opsForValue().get(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originUrl)) {
                //统计数据
                shortLinkStats(fullShortUrl, request, response);
                // 302重定向
                ((HttpServletResponse) response).sendRedirect(originUrl);
                return;
            }
            //数据库查询
            LambdaQueryWrapper<ShortLinkGotoDO> shortLinkGotoDOLambdaQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(shortLinkGotoDOLambdaQueryWrapper);
            if (shortLinkGotoDO == null) {
                //缓存穿透：5、添加空对象
                redisTemplate.opsForValue().set(String.format(GOTO_SHORT_IS_NULL_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 1);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().isBefore(LocalDateTime.now()))) {
                redisTemplate.opsForValue().set(String.format(GOTO_SHORT_IS_NULL_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/expired");
                return;
            }
            // 将短链接和原始链接的映射存到Redis
            redisTemplate.opsForValue().set(String.format(RedisConstant.GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()
                    , LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS);
            //统计数据
            shortLinkStats(fullShortUrl, request, response);
            // 302重定向
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            lock.unlock();
        }
    }

    /**
     * 记录短链接基础访问记录
     */
    public void shortLinkStats(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        AtomicReference<String> uv = new AtomicReference<>();

        Cookie[] cookies = ((HttpServletRequest) request).getCookies();

        Runnable addCookie = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie cookie = new Cookie("uv", uv.get());
            //一个月
            cookie.setMaxAge(60 * 60 * 24 * 30);
            cookie.setPath(StrUtil.subAfter(fullShortUrl, "/", true));
            ((HttpServletResponse) response).addCookie(cookie);
            uvFirstFlag.set(Boolean.TRUE);
        };

        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(cookie -> "uv".equals(cookie.getName()))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdd = redisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                        uvFirstFlag.set(uvAdd != null && uvAdd > 0L);
                    }, addCookie);
        } else {
            addCookie.run();
        }

        String remoteAddr = getActualIp((HttpServletRequest) request);
        Long uipAdd = redisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdd != null && uipAdd > 0L;

        ShortLinkAccessStatsDO statsDO = ShortLinkAccessStatsDO.builder()
                .pv(1)
                .uv(uvFirstFlag.get() ? 1 : 0)
                .uip(uipFirstFlag ? 1 : 0)
                .hour(LocalDateTime.now().getHour())
                .weekday(LocalDateTime.now().getDayOfWeek().getValue())
                .fullShortUrl(fullShortUrl)
                .date(LocalDate.now())
                .build();
        shortLinkAccessStatsMapper.shortLinkStats(statsDO);

        //统计地区
        Map<String, Object> localParamMap = new HashMap<>();
        localParamMap.put("key", amapKey);
        localParamMap.put("ip", remoteAddr);
        String localResultStr = HttpUtil.get("https://restapi.amap.com/v3/ip", localParamMap);
        JSONObject localResultObj = JSON.parseObject(localResultStr);
        String infoCode = localResultObj.getString("infocode");
        LinkLocaleStatsDO linkLocaleStatsDO;
        if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
            String province = localResultObj.getString("province");
            boolean unKnowFlag = StrUtil.equals(province, "[]") || StrUtil.equals(province, "局域网");
            linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                    .province(unKnowFlag ? "未知" : province)
                    .city(unKnowFlag ? "未知" : localResultObj.getString("city"))
                    .adcode(unKnowFlag ? "未知" : localResultObj.getString("adcode"))
                    .fullShortUrl(fullShortUrl)
                    .cnt(1)
                    .country("中国")
                    .date(LocalDate.now())
                    .build();
            linkLocaleStatsMapper.shortLinkStats(linkLocaleStatsDO);

            //获取OS
            String os = getOs((HttpServletRequest) request);
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .os(os)
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(LocalDate.now())
                    .build();
            linkOsStatsMapper.shortLinkOsStats(linkOsStatsDO);

            //获取浏览器
            String browser = LinkUtil.getBrowser((HttpServletRequest) request);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .browser(browser)
                    .cnt(1)
                    .date(LocalDate.now())
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserStats(linkBrowserStatsDO);

            //添加高频访问IP统计
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(uv.get())
                    .ip(remoteAddr)
                    .fullShortUrl(fullShortUrl)
                    .browser(browser)
                    .os(os)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);

            //添加设备统计
            String device = LinkUtil.getDevice((HttpServletRequest) request);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(device)
                    .cnt(1)
                    .date(LocalDate.now())
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkDeviceStatsMapper.linkDeviceStats(linkDeviceStatsDO);

            //获取访问网络
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

        }
    }


    /**
     * 根据原始链接创建短链接后缀
     */
    public String generateSuffix(ShortLinkCreateDTO requestParam) {
        int customGenerateCount = 0;
        String suffix;
        while (true) {
            if (customGenerateCount >= 10) {
                throw new ClientException("短链接创建繁忙，稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            //重试的时候能改变hash值
            originUrl += System.currentTimeMillis();
            suffix = HashUtil.hashToBase62(originUrl);
            //此处的布隆过滤器判断不存在的情况，没有误判情况
            if (!shortLinkCreateCachePenetrationBloomFilter.contains(domain + "/" + suffix)) {
                break;
            }
            customGenerateCount++;
        }
        return suffix;
    }
}




